#ifndef __tenacitas_log__logger__h__
#define __tenacitas_log__logger__h__


/** @file
    In this file is defined the @p tenacitas::log::logger class
*/

/** @todo */


// ==> C++ Headers
#include <string>
#include <cstdint>
#include <utility>
#include <memory>
#include <thread>
#include <functional>

// ==> 3rds Headers

// ==> Our Headers
/** In the <tt>level.h</tt> file we have the definition of the log levels  */
#include "level.h"
#include "thread_id.h"
#include <tenacitas.exception/exception.h>

// ==> Namespaces
using namespace std;

// ==> Macro Constants
/** */

// ==> Macro Commands

/** This macro allows a message to be logged in @p tenacitas::log::level::trace level

    @param  logger is an instance of some specialization of @p
    tenacitas::log::logger class */
#define log_trace(logger) logger.new_line(tenacitas::log::level::trace, __FILE__, __func__, __LINE__)

/** This macro allows a message to be logged in @p tenacitas::log::level::debug level 

    @param  logger is an instance of some specialization of @p
    tenacitas::log::logger class */
#define log_debug(logger) logger.new_line(tenacitas::log::level::debug, __FILE__, __func__, __LINE__)

/** This macro allows a message to be logged in @p tenacitas::log::level::info level 

    @param  logger is an instance of some specialization of @p
    tenacitas::log::logger class */
#define log_info(logger) logger.new_line(tenacitas::log::level::info, __FILE__, __func__, __LINE__)

/** This macro allows a message to be logged in @p tenacitas::log::level::warn level 

    @param  logger is an instance of some specialization of @p
    tenacitas::log::logger class */
#define log_warn(logger) logger.new_line(tenacitas::log::level::warn, __FILE__, __func__, __LINE__)

/** This macro allows a message to be logged in @p tenacitas::log::level::error level 

    @param  logger is an instance of some specialization of @p
    tenacitas::log::logger class */
#define log_error(logger) logger.new_line(tenacitas::log::level::error, __FILE__, __func__, __LINE__)

/** This macro allows a message to be logged in @p tenacitas::log::level::fatal
    level, and causes the program to exit

    @param  logger is an instance of some specialization of @p
    tenacitas::log::logger class

    @param msg is the message to be logged before the program to exit
*/  
#define log_fatal(logger, msg) logger.new_line(tenacitas::log::level::fatal, __FILE__, __func__, __LINE__) << msg; \
    exit(1);                                                            \
    



/** ==> namespace of the organization */
namespace tenacitas {

    /** ==> namespace of the project */
    namespace log {

        // ==> Typedefs

        // ==> Pre-Declarations

        // ==> Attributes

        // ==> Functions
        
        // ==> Classes

        /** Base class that provides logging facilities

            The main goal of this class is to log messages without the need of
            some concurrent access protection, like mutual exclusion
            objects. This is accomplished by separeting log messages by
            thread. Within the same thread, log messsages can be logged, no
            matter the media where they are being logged, without concurrence.

            For instance, if the media where the log messages will be saved is
            file, a log file can be created for each thread. So, all the messages
            for a thread will be sequentially logged in the same file, without
            concurrence. 

            @tparam t_supplier is responsible for supplying types so that @p
            logger class can be fully defined. The types are:

            
            @tparam t_supplier::media_factory, which defines the media where
            the message logs will be written, and creates media objects. @p
            media_factory must provide:
            @code
            typedef media, the concrete media that @p media_factory defines.

            media create(thread_id), a @p media object, when it is informed of
            the thread. 
            @endcode

            @tparam t_supplier::media_factory::media must implement:
            @code
            template <typename t_data>
            void write(const t_data & p_data), which writes any kind of
            data to the media.

            void new_line(std::chrono::nanoseconds::rep p_nanosecs,
            level p_level,
            const char * p_file,
            const char * p_function,
            int64_t p_line), which should start a new line, or
            log record. When a new line starts, it is informed the timestamp (@p
            p_namosecs), the log level (@p p_level), the name of the file (@p
            p_file), the function (@p p_function), and line number (@p p_line),
            where the new log line is being generated.

            media (media && p_media), a move constructor.
            @endcode

            Here is a example of a very trivial implementation of @p media, 
            @p media_factory and @p supplier classes:

            @code
            struct simple_media {
            template <typename t_data>
            void write(const t_data & p_data) {
            std::cout << "|" << p_data;
            }

            void new_line(std::chrono::nanoseconds::rep p_nanosecs,
            level p_level,
            const char * p_file,
            const char * p_function,
            int64_t p_line) {
            std::cout << std::endl
            << p_th_id << "|"
            << p_nanosecs << "|"
            << p_level.str() << "|"
            << p_file << "|"
            << p_function << "|"
            << p_line;
            }

            simple_media(thread_id p_th_id)
            : m_th_id(p_th_id){}

            simple_media (simple_media && p_media){
            m_th_id(std::move(p_media.m_th_id));
            }
            private:
            thread_id m_th_id;
            };

            template <typename t_supplier>
            struct simple_media_factory {
            typedef simple_media media;

            media create(std::thread::id p_th_id) {
            return media(p_th_id);
            }
            };            
            @endcode
            
            @p <b>logger</b> template class needs a factory to create objects, so
            @p <b>t_supplier</b> must also supply @p logger_factory, which
            actually creates @logger objects.
            
            @tparam <b>t_supplier::logger_factory</b> must define:

            @code
            logger create(level), which provides a @p logger object, defining its
            log level. 
            @endcode

            A very trivial implementation of a @p logger_factory:
            @code
            template <typename t_supplier>
            struct simple_logger_factory {
            typedef typename tenacitas::log::logger<t_supplier> logger;

            logger create(level p_level = level::info) {
            m_logger.set_level(p_level);
            return m_logger;
            }

            private:
            logger m_logger;
            };
            @endcode


            A @p<b>supplier</b> class that groups the factories above:
            @code
            struct simple_supplier {

            typedef simple_media_factory<supplier> media_factory;
            typedef simple_logger_factory<supplier> logger_factory;

            media_factory & get_media_factory() {
            return m_media_factory;
            }

            logger_factory & get_logger_factory() {
            return m_logger_factory;
            }
            private:
            media_factory m_media_factory;
            logger_factory m_logger_factory;
            };

            @endcode

            <b>A word to the programmer user</b>
            You, dear programmer, needs to call <tt>static void
            configure(std::shared_ptr<supplier>)</tt> in order to inform @p
            logger the @p supplier object.

            You should avoid using the tenacitas::log::logger::new_line and <tt>
            operator<< </tt> members, and use the macros commands @p log_trace,
            @p log_debug, @p log_info, @p log_warn, @p log_error or @p log_fatal
            instead. These macros are defined in the beginning of this header
            file. 

            You should use the static method tenacitas::log::logger::set_level_all
            to define the tenacitas::log::level to be used by all the @p logger
            objects.

            You can change the tenacitas::log::level for all the @p logger objects
            to be created (not the ones already created), by calling
            tenacitas::log::logger::set_level_all again.

            When creating a @p logger, you can define a tenacitas::log::level to be
            used by only for it, as you can see in the @p logger_factory example
            above.

            You would better call tenacitas::log::logger::can_log before actualy
            logging a message to improve performance.

            Here it is a piece of code that illustrates how to use the classes
            defined above:

            @code
            class t0 {
            public:
            typedef simple_supplier::logger_factory logger_factory;

            typedef logger_factory::logger logger;
                
            void run() {
            logger & l_logger = m_logger_factory.create();
            log_info(l_logger) << "ola" << 456.12;
            }

            private:
            logger_factory m_logger_factory;
            };
            @endcode

            The code above would print in <tt>std::cout</tt> the line:
            <tt>
            3074926912|1410965404709692062|info|test0.h|run|118|ola|456.12
            </tt>

            Where @p 3074926912 is the thread id, @p 1410965404709692062 is the
            time stamp, @p info is the log level, <tt>test0.h</tt> is the file
            name, @p run is the function name, @p 118 is the line number where
            the message log was created, and @p ola and <tt>456.12</tt> are the
            contents of the log message.

            @nosubgrouping
        */
        template <typename t_supplier>
        class logger {
        public:

            // ==> logger - Public Internal Types

            /** Responsible for supplying types so that @p logger class can be
                fully defined */
            typedef t_supplier supplier;

            /** */
            typedef typename supplier::media_factory media_factory;

            /** */
            typedef typename media_factory::media media;

            /** */
            typedef typename supplier::logger_factory logger_factory;

            // ==> logger - Friends

            /** */
            friend logger_factory;

            // ==> logger - Public Constructors

            /** Configure the logger, defining the @p supplier object */
            static void configure(std::shared_ptr<supplier> p_supplier);

            /** Destructor */
            ~logger();

            /** Copies are not-allowed */
            logger (const logger & p_logger) = delete;

            /** Moves are not-allowed */
            logger (logger && p_logger) = delete;

            // ==> logger - Public Accessors

            /** Resets the log level defined to this @p logger object

                @param  p_level is the new tenacitas::log::level to be used
                by this @p logger object. */
            void set_level(const level & p_level);

            /** @return the log level for this @p logger object */
            const level & get_level() const;

            /** @return the id of the thread where the log was created */
            std::hash<std::thread::id> get_thread_id() const;

            /** sets the log level for all @p logger objects */
            static void set_level_all(const level & p_level);

            /** @return the log level for all @p logger objects */
            static const level & get_level_all();


            /** Informs if a message log can be logged

                Before logging anything, one should, for performance sake, verify
                if the message will actualy be logged.
                <b>"Why?"</b>, you may ask
                Well, suppose a log such as:
                <tt> log_debug(l_log) << "very long string.."
                << a_double
                << "another long string";
                </tt>
                If the log level for the @p l_log object is
                tenacitas::log::level::error, nothing will be logged. So, the work
                done to parse the parts of the log message will have been for
                nothing! Therefore, one should write:
                <tt>
                if (l_log->can_log(tenacitas::log::level::debug) {
                log_debug(l_log) << "very long string.."
                << a_double
                << "another long string";
                }
                </tt>

                @param  p_level is the level one wants to check if a log
                will be logged

                @attention <b>tenacitas::log::level::error and
                tenacitas::log::level::fatal will allways be logged, no matter
                the current log level. So, it is not necessary to check for the
                log level if one wants to use @p log_error or @p log_fatal</b>
            */
            bool can_log(const level & p_level) const;

            // ==> logger - Public Processors

            /** Starts a new log line

                @attention: One should not call this method directly, but use the
                @p log_trace, @p log_debug, @p log_info, @p log_warn,
                @p logic_error or @p log_fatal

                This method informs the @p media class that a new line is
                starting. Depending on the concrete type of the media, this can
                mean a new line in a file, a new record in a database table, or
                anything else.

                When a new log line is about to be created, if @p logger realizes
                that the thread is not the same anymore, it requests @p
                logger_factory to create a new @p logger object, and that is the
                one returned from the method.

                @param  p_level is the tenacitas::log::level of the new line
                @param  p_file is the source file where the log is occurring
                @param  p_function is the function (method) that is log is
                occurring
                @param  p_line is the line it the @p p_file where the log is
                occurring
                @return the same @p logger object, if the thread is still the
                same; or a new @p loggger object, if the thread changed
            */
            logger & new_line(level p_level,
                              const char * p_file,
                              const char * p_function,
                              int64_t p_line);

            // ==> logger - Public Operators

            /** */
            void *operator new[] (size_t) = delete;

            /** */
            void operator delete[] (void *) = delete;

            /** */
            logger & operator=(const logger &)=delete;

            /** */
            logger && operator=(logger &&)=delete;

            /** not-allowed */
            void* operator new (std::size_t size) throw (std::bad_alloc) = delete;

            /** not-allowed */
            void operator delete (void *p_logger) = delete;

            /** Outputs any kind of data

                @tparam t_data is the type of the data to be logged

                @param p_data is the data to be logged
            */
            template <typename t_data>
            logger & operator << (const t_data & p_data);

            /** Outputs any kind of data

                @tparam t_data is the type of the data to be logged

                @param p_data is the data to be logged
            */
            template <typename t_data>
            logger & operator << (t_data && p_data);

        private:

            // ==> logger - Private Types

            // ==> logger - Private Constructor

            /** Constructor

                @param  p_level is the tenacitas::log::level to be used
                by this @p logger object. Its default value is the log level
                defined for all the @p logger objects, defined using the
                @p set_level_all below, and retrieved via @p get_level_all

                @note Please, read the documentation for tenacitas::log::level
                to understand how log levels work in tenacitas::log

            */
            logger(level p_level = logger::get_level_all());

            // /** Only @p logger_factory can */
            // void* operator new (std::size_t size) throw (std::bad_alloc) {
            //     return ::new logger;
            // }

            // /** */
            // void operator delete (void *p_logger) {
            //     ::delete(static_cast<logger*>(p_logger));
            // }



            // ==> logger - Private Attributes

            /** Thread id of the thread for whom log messages are being created */
            thread_id m_thread_id;

            /** Log level of this @p logger object */
            level m_level;

            /** Log level of the current log line */
            level m_line_level;


            /** Class that actually logs the message */
            media m_media;

            /** Log level commom to all the @p logger objects */
            static level m_level_all;

            /** Supplier of all the object needed by @p logger */
            static std::shared_ptr<supplier> m_supplier;
            
        };
    }
}

// =============================================================================
//                     I M P L E M E N T A T I O N
// =============================================================================

using namespace tenacitas::log;

// #############################################################################
// logger

// -----------------------------------------------------------------------------
template <typename t_supplier>
level
logger<t_supplier>::
m_level_all(level::info);

// -----------------------------------------------------------------------------
template <typename t_supplier>
std::shared_ptr<t_supplier>
logger<t_supplier>::
m_supplier;

// -----------------------------------------------------------------------------
template <typename t_supplier>
void
inline logger<t_supplier>::
configure(std::shared_ptr<t_supplier> p_supplier) {m_supplier = p_supplier;}


// -----------------------------------------------------------------------------
template <typename t_supplier>
inline logger<t_supplier>::
~logger() { }


// -----------------------------------------------------------------------------
template <typename t_supplier>
inline void
logger<t_supplier>::
set_level(const level & p_level) {
    m_level = p_level;
}

// -----------------------------------------------------------------------------
template <typename t_supplier>
inline const level &
logger<t_supplier>::
get_level() const {
    return m_level;
}

// -----------------------------------------------------------------------------
template <typename t_supplier>
std::hash<std::thread::id>
logger<t_supplier>::
get_thread_id() const {return m_thread_id;}


// -----------------------------------------------------------------------------
template <typename t_supplier>
inline void
logger<t_supplier>::
set_level_all(const level & p_level) {
    m_level_all = p_level;
}

// -----------------------------------------------------------------------------
template <typename t_supplier>
inline const level &
logger<t_supplier>::
get_level_all() {
    return m_level_all;
}

// -----------------------------------------------------------------------------
template <typename t_supplier>
inline bool
logger<t_supplier>::
can_log(const level & p_level) const {
    if ((p_level == level::error) || (p_level == level::fatal)) {
        return true;
    }
    return (p_level >= m_level);
}


// -----------------------------------------------------------------------------
template <typename t_supplier>
logger<t_supplier>::
logger(level p_level)
    : m_thread_id(),
      m_level(p_level),
      m_line_level(p_level),
      m_media(std::move(m_supplier->get_media_factory().create(m_thread_id))) {}

// -----------------------------------------------------------------------------
template <typename t_supplier>
logger<t_supplier> &
logger<t_supplier>::
new_line(level p_level,
         const char * p_file,
         const char * p_function,
         int64_t p_line) {

    try {

        m_line_level = p_level;

        // check if it is still in the same thread as it was created
        if (m_thread_id != thread_id()) {

            // NO! So we create a logger associated to the current
            // thread

            logger & l_logger = m_supplier->get_logger_factory().create(p_level);

            // but, the requested log level is allowed now?
            if (l_logger.can_log(p_level)) {
                // ok! so we ask the @p m_media to cerate a new line
                l_logger.m_media.new_line(std::chrono::nanoseconds(std::chrono::high_resolution_clock::now().time_since_epoch()).count(),
                                          p_level, p_file,
                                          p_function, p_line);
            }
            return l_logger;
        }
        else {
            // the current thread is the same as the one where this logger
            // object was created
            if (this->can_log(p_level)) {
                // ok! so we ask the @p t_media object to actually
                // create a new log line
                m_media.new_line(std::chrono::nanoseconds(std::chrono::high_resolution_clock::now().time_since_epoch()).count(),
                                 p_level, p_file,
                                 p_function, p_line);
            }
            return *this;
        }

    }
    catch (std::exception & l_ex) {
        tenacitas::exception::exception
            l_tenacitas_ex(make_tenacitas_exception(l_ex.what()));

        std::cerr << l_tenacitas_ex.what() << std::endl;

        throw (l_tenacitas_ex);
    }

    tenacitas::exception::exception
        l_tenacitas_ex(make_tenacitas_exception("Impossible situation: could not "
                                                "find a suitable logger object "
                                                "to create a new line"));

    std::cerr << l_tenacitas_ex.what() << std::endl;

    throw (l_tenacitas_ex);
}

// -----------------------------------------------------------------------------
template <typename t_supplier>
template <typename t_data>
logger<t_supplier> &
logger<t_supplier>::
operator << (const t_data & p_data) {
    try {
        if (can_log(m_line_level)) {
            m_media.write(p_data);
        }
    }
    catch(std::exception & l_ex) {
        tenacitas::exception::exception
            l_tenacitas_ex(make_tenacitas_exception(l_ex.what()));

        std::cerr << l_tenacitas_ex.what() << std::endl;

        throw (l_tenacitas_ex);
    }
    return *this;
}

// -----------------------------------------------------------------------------
template <typename t_supplier>
template <typename t_data>
logger<t_supplier> &
logger<t_supplier>::
operator << (t_data && p_data) {
    try {
        if (can_log(m_line_level)) {
            m_media.write(p_data);
        }
    }
    catch(std::exception & l_ex) {
        tenacitas::exception::exception
            l_tenacitas_ex(make_tenacitas_exception(l_ex.what()));

        std::cerr << l_tenacitas_ex.what() << std::endl;

        throw (l_tenacitas_ex);
    }
    return *this;
}


#endif
